// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/bithumb/api_order/native_private_client.h"

#include <list>
#include <memory>
#include <string>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/base/order/rate_config.h"
#include "coin2/exchange/bithumb/symbology/product.h"

constexpr auto EXAMPLE_SPOT_BITHUMB = R"(
"connection_config": {
  "key_filepath": "/home/ec2-user/config/trade/xunke06/bithumb/trade_key.json",
  "rate_limit_info": {
    "spot_bithumb": {
      "req_all": {
        "secs": 1,
        "reqs": 135
      }
    }
  }
})";

using coin2::exchange::bithumb::symbology::BithumbProduct;

auto BithumbNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  auto params = RestParams();

  BithumbProduct product =
      BithumbProduct::FromStr(order_spec.product, order_spec.order_created_time);
  params.set_order_currency(product.base().native_currency());
  params.set_payment_currency(product.quote().native_currency());
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_units(formatter.FormatQty(order_spec.qty));
  params.set_price(formatter.FormatPrice(order_spec.price));

  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_type("bid");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_type("ask");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }

  return params;
}

auto BithumbNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  CHECK(order_context.has_exchange_order_id());
  auto params = RestParams();
  params.set_order_id(order_context.exchange_order_id());
  BithumbProduct product = BithumbProduct::FromStr(
      order_context.order_spec().product,
      order_context.order_spec().order_created_time);
  params.set_order_currency(product.base().native_currency());
  params.set_payment_currency(product.quote().native_currency());
  if (order_context.order_side() == OrderSide::BUY_ORDER) {
    params.set_type("bid");
  } else if (order_context.order_side() == OrderSide::SELL_ORDER) {
    params.set_type("ask");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_context.order_side());
  }
  return params;
}

BithumbNativePrivateClient::BithumbNativePrivateClient(
    const MarketExchangeApi& mea,
    const OrderConnectionConfig& config_override) {
  mea_ = mea;
  OrderConnectionConfig config;
  config.set_rest_host("club.bithumb.com");  // api.bithumb.com
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[BithumbNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<BithumbRestAuth, OrderConnectionConfig>(config);

  // rate limit tracker
  using RateConfig = coin2::exchange::base::order::RateConfig;
  coin2::exchange::order::RateLimitInfoSpotBithumb rate_config;
  RateConfig::Get()->GetConfig(mea_, config, &rate_config);
  if (!rate_config.has_req_all()) {
    RateConfig::CheckFail(mea_.String(), EXAMPLE_SPOT_BITHUMB);
  }
  const auto& req_all = rate_config.req_all();
  LOG(INFO) << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
            << "\nOG request rate was set as below:\n"
            << "\nreq_all: [" << req_all.reqs() << " (reqs) / " << req_all.secs() << " (seconds)]"
            << "\n\nOr you can configure customized rate in driver config. For example: "
            << EXAMPLE_SPOT_BITHUMB
            << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~";
  rate_tracker_ = std::make_unique<RateTracker>(req_all.secs() * 1'000'000'000LL);
  rate_tracker_->SetRateLimitInfo(req_all.reqs(), req_all.reqs());
  rate_tracker_->SetRateLimitInfoString(&rate_config);
}

void BithumbNativePrivateClient::Prepare() {
  mb_->Subscribe("bithumb/req_rate_limit", [this](const std::string&, void* data) {
    auto info = nlohmann::json::parse(*reinterpret_cast<std::string*>(data));
    int req_rate_limit_remaining = info["X-RateLimit-Remaining"].get<int>();
    int req_rate_limit_capacity = info["X-RateLimit-Burst-Capacity"].get<int>();
    if (req_rate_limit_remaining < 0 || req_rate_limit_capacity <= 0 ||
        req_rate_limit_remaining > req_rate_limit_capacity) {
      return;
    }
    this->rate_tracker_->SetRateLimitInfo(req_rate_limit_capacity, req_rate_limit_remaining);
  });
}

std::shared_ptr<AsioHttpContext> BithumbNativePrivateClient::QueryBalance(
    const RestParams& params) {
  // auto ts = GetCurrentTimestamp();
  // if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
  //   DLOG(ERROR) << "Rate limit! QueryBalance not submit!";
  //   return {};
  // }

  UrlParamsBuilder builder;
  builder.PutPost("currency", params.currency());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/info/balance", &builder);
  // Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BithumbNativePrivateClient::QueryOpenOrders(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "Rate limit! QueryOpenOrders not submit!";
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutPost("order_currency", params.order_currency());
  builder.PutPost("payment_currency", params.payment_currency());
  builder.PutPost("count", params.count());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/info/orders", &builder);
  context->extra_info = params.order_currency() + "_" + params.payment_currency();
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BithumbNativePrivateClient::QueryTrades(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "Rate limit! QueryTrades not submit!";
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutPost("count", params.count());
  builder.PutPost("order_currency", params.order_currency());
  builder.PutPost("payment_currency", params.payment_currency());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/info/user_transactions", &builder);
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BithumbNativePrivateClient::QueryTradesV2(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "Rate limit! QueryTrades not submit!";
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutPost("order_id", params.order_id());
  builder.PutPost("order_currency", params.order_currency());
  builder.PutPost("payment_currency", params.payment_currency());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/info/order_detail", &builder);
  context->extra_info = params.order_id();
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BithumbNativePrivateClient::PlaceOrder(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::PLACE)) {
    LOG(ERROR) << "Rate limit! PlaceOrder not submit!";
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutPost("order_currency", params.order_currency());
  builder.PutPost("payment_currency", params.payment_currency());
  builder.PutPost("price", params.price());
  builder.PutPost("units", params.units());
  builder.PutPost("type", params.type());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/trade/place", &builder);
  Push(1, ts, ActionType::PLACE);

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BithumbNativePrivateClient::CancelOrder(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::CANCEL)) {
    LOG(ERROR) << "Rate limit! CancelOrder not submit!";
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutPost("order_currency", params.order_currency());
  builder.PutPost("payment_currency", params.payment_currency());
  builder.PutPost("type", params.type());
  builder.PutPost("order_id", params.order_id());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/trade/cancel", &builder);
  Push(1, ts, ActionType::CANCEL);

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BithumbNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

std::shared_ptr<AsioHttpContext> BithumbNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params);
}

std::shared_ptr<AsioHttpContext> BithumbNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> BithumbNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryBalance(params);
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      return QueryOpenOrders(params);
    }
    default: {
      return nullptr;
    }
  }
}
